import path from 'node:path';
import fs from 'node:fs';
import { ESLint } from 'eslint';
import { program } from 'commander';
import * as prettier from 'prettier';
import kebabCase from 'lodash/kebabCase.js';
import camelCase from 'lodash/camelCase.js';
import sortBy from 'lodash/sortBy.js';
import eslintConfig from '../../eslint.config.mjs';

const ROOT_PATH = path.resolve(import.meta.dirname, '../../');

function createESLintInstance(overrideConfig) {
  return new ESLint({ overrideConfigFile: true, overrideConfig, fix: false });
}

function lint(eslint, filePaths) {
  return eslint.lintFiles(filePaths);
}

/**
 * Creates a barebone ESLint config to lint the codebase with. We only keep configs that make use
 * of the rule we are generating a todo file for. If no config use the rule, this returns `null` and
 * should cause the script to abort its execution.
 *
 * @param {string} rule The rule to generate a todo file for.
 * @returns {object|null} The config to use for the rule.
 */
function getConfigForRule(rule) {
  let configHasRule = false;
  const newConfig = eslintConfig
    .map((config) => {
      // We preserve existing configs for the rule so that we don't add valid files to the todo file.
      // However, we bypass configs that disabled the rule as those are likely the todo files themselves.
      const hasRuleDefinition = config.rules?.[rule] && config.rules[rule] !== 'off';
      if (hasRuleDefinition) {
        configHasRule = true;
        return {
          ...config,
          rules: {
            [rule]: config.rules[rule],
          },
        };
      }

      return {
        ...config,
        rules: {},
      };
    })
    .filter((config) => config !== null);

  if (configHasRule) {
    return newConfig;
  }
  return null;
}

function getOffendingFiles(results, rule) {
  return results.reduce((acc, result) => {
    const hasRuleError = result.messages.some((message) => message.ruleId === rule);
    if (hasRuleError) {
      acc.push(result.filePath);
    }
    return acc;
  }, []);
}

async function prettify(data) {
  const prettierConfig = await prettier.resolveConfig(path.join(ROOT_PATH, '.prettierrc'));
  return prettier.format(data, {
    ...prettierConfig,
    parser: 'babel',
  });
}

async function writeTodoFile(rule, offendingFiles) {
  const slugifiedRule = kebabCase(rule);
  const todoFileName = `${slugifiedRule}.mjs`;
  const todoFilePath = path.join(ROOT_PATH, '.eslint_todo', todoFileName);
  const camelCasedRule = camelCase(rule);
  const relativePaths = sortBy(offendingFiles.map((file) => path.relative(ROOT_PATH, file)))
    .map((relativePath) => `'${relativePath}'`)
    .join(',\n');
  const indexFilePath = path.join(ROOT_PATH, '.eslint_todo', 'index.mjs');
  const indexFileContent = fs.readFileSync(indexFilePath, { encoding: 'utf-8' });

  console.log(`Writing todo file to ${todoFilePath}.`);

  const newConfig = `
    /**
     * Generated by \`node scripts/frontend/generate_eslint_todo_list.mjs ${rule}\`.
     */
    export default {
      files: [${relativePaths}],
      rules: {
        '${rule}': 'off',
      },
    }
    `;

  const formattedTodoFileContent = await prettify(newConfig);

  fs.writeFileSync(todoFilePath, formattedTodoFileContent);

  if (!indexFileContent.match(camelCasedRule)) {
    console.log(`Adding export statement to ${indexFilePath}.`);
    const exportStatement = `export { default as ${camelCasedRule} } from './${todoFileName}';`;
    const newIndexFileContent = `${indexFileContent}\n${exportStatement}`;
    const formattedNewIndexFileContent = await prettify(newIndexFileContent);
    fs.writeFileSync(indexFilePath, formattedNewIndexFileContent);
  } else {
    console.log(`Export statement already exists in ${indexFilePath}.`);
  }
}

async function main() {
  program
    .description(
      'Generates a todo file to skip linting on offending files for a specific ESLint rule.',
    )
    .option('--debug-config', 'Prints the ESLint config used to generate the todo file.')
    .argument('<rule>')
    .parse(process.argv);
  const options = program.opts();
  const [rule] = program.args;

  console.log(`Generating todo file for rule \`${rule}\`...`);

  const overrideConfig = getConfigForRule(rule);

  if (overrideConfig === null) {
    console.error(
      `The rule \`${rule}\` could not be found in the ESLint configuration. It needs to be enabled before generating a todo file.`,
    );
    process.exitCode = 1;
    return;
  }

  if (options.debugConfig) {
    console.log('Using ESLint configuration:');
    console.log(overrideConfig);
  }

  const eslint = createESLintInstance(overrideConfig);
  const results = await lint(eslint, [
    './app/assets/javascripts/**/*.{js,mjs,cjs,vue}',
    './ee/app/assets/javascripts/**/*.{js,mjs,cjs,vue}',
    './spec/frontend/**/*.js',
    './ee/spec/frontend/**/*.js',
    'scripts/**/*.{js,mjs,cjs}',
  ]);

  const offendingFiles = getOffendingFiles(results, rule);
  if (offendingFiles.length > 0) {
    console.log(`Found ${offendingFiles.length} offending files.`);
    await writeTodoFile(rule, offendingFiles);
  } else {
    console.error('No offenses found. Delete any existing todo file if it is not needed anymore.');
    process.exitCode = 1;
  }
}

main();
